Skip to content

Conversation

@bbbang105
Copy link
Member

@bbbang105 bbbang105 commented Jan 20, 2026

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명
    변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

🚀 작업 내용

이번 PR에서 작업한 내용을 구체적으로 설명해주세요. (이미지 첨부 가능)

RefreshToken MySQL 마이그레이션

  • Redis에서 관리하던 RefreshToken을 MySQL로 마이그레이션
  • Token Rotation + Grace Period 패턴 적용으로 토큰 탈취 감지 기능 추가
  • family_id 기반 토큰 계보 관리로 탈취 시 전체 무효화 가능
  • 기존 분산락(@DistributedLock) 및 쿨다운 로직 제거

토큰 재발급 API 개선

  • 원자적 업데이트(markAsRotatedIfActive) 패턴 적용
  • Grace Period (3초) 내 중복 요청은 429 에러 처리
  • Grace Period 초과 재사용 시 토큰 탈취로 감지 → family 전체 revoke

클라이언트 정보 로깅

  • ClientInfoExtractor 유틸리티 추가 (IP, User-Agent 추출)
  • 토큰 발급/재발급 시 IP, User-Agent 저장으로 감사 추적 가능
  • 온보딩 시 null로 저장되던 문제 수정

스케줄러 추가

  • 만료 토큰 상태 업데이트 스케줄러 (ACTIVE → EXPIRED)
  • 오래된 비활성 토큰 soft delete 스케줄러 (30일 보관 후 DELETED)
  • cron 및 retention-days 설정을 YAML로 외부화

테스트 환경 개선

  • H2에서 Testcontainers(MySQL)로 전환
  • Singleton Container Pattern 적용으로 테스트 성능 최적화
  • RefreshToken 관련 통합 테스트 코드 추가

CI/CD 개선

  • commit-labeler에 ci 라벨 추가
  • prod-cicd가 PR 머지 시에만 실행되도록 수정

📝️ 관련 이슈

본인이 작업한 내용이 어떤 Issue와 관련이 있는지 작성해주세요.


💬 기타 사항 or 추가 코멘트

남기고 싶은 말, 참고 블로그 등이 있다면 기록해주세요.

참고 자료

Summary by CodeRabbit

  • 새 기능

    • 리프레시 토큰 회전·가족 단위 라이프사이클 관리 및 DB 기반 영속화
    • 클라이언트 IP/UA 추출 유틸 및 리프레시 토큰 정리 스케줄러 추가
  • 버그 수정

    • 토큰 재사용·중복 요청 탐지 강화 및 대응 로직 추가
    • JWT 필터 예외 경로 확장
  • 테스트

    • Testcontainers 기반 DB 테스트 인프라 및 리포지토리/서비스 테스트 대거 추가
  • 개선사항 / 기타

    • CI: PR 병합 시에만 배포 단계 실행
    • 커밋 라벨링 범위 확대(추가 라벨: test, perf, ci)
    • Redis 기반 분산 락 및 관련 구성 제거, 자동 리뷰어 할당 워크플로우 추가

✏️ Tip: You can customize this high-level summary in your review settings.

bbbang105 and others added 11 commits January 20, 2026 14:34
- RefreshToken 엔티티에 token rotation 관련 필드 추가
- TokenStatus enum 추가 (ACTIVE, REVOKED, EXPIRED, ROTATED)
- 원자적 업데이트를 위한 markAsRotatedIfActive 메서드 추가
- QueryDSL 기반 커스텀 Repository 구현 (revoke, hard delete)
- 새로운 에러 코드 추가 (_TOKEN_REUSE_DETECTED, _DUPLICATED_REQUEST, _ALREADY_USED_REFRESH_TOKEN)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Race condition 방지를 위한 원자적 토큰 상태 업데이트
- 토큰 값 검증 로직 추가 (DB 토큰과 요청 토큰 비교)
- Grace Period 내 중복 요청 처리
- 토큰 재사용 공격 탐지 시 family 전체 revoke

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- ClientInfoExtractor 유틸 클래스 추가 (IP, UserAgent 추출)
- OAuth 로그인 및 온보딩 시 클라이언트 정보 저장
- 테스트 로그인에도 클라이언트 정보 저장 적용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 회원 탈퇴(withdraw) 시 해당 유저의 모든 ACTIVE 토큰을 REVOKED로 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- calculateRefreshTokenExpiryAt 메서드 추가
- 토큰 만료 시간 계산 로직 중복 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 만료된 토큰 상태 업데이트 (ACTIVE → EXPIRED) 스케줄러 추가
- 오래된 비활성 토큰(REVOKED/EXPIRED/ROTATED) hard delete 스케줄러 추가
- 스케줄러 cron 및 retention-days 설정을 YAML로 외부화
- 기본값: 매일 03:00 만료 처리, 03:30 hard delete, 30일 보관

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Testcontainers MySQL 의존성 추가 (H2 제거)
- Singleton Container Pattern으로 DatabaseTestConfig 구현
- 테스트 환경에서 실제 MySQL과 동일한 환경으로 테스트 가능
- @Serviceconnection과 @DynamicPropertySource로 DB 연결 자동화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- RefreshTokenRepositoryTest: Repository 레이어 통합 테스트
- TokenServiceTest: 토큰 재발급, 검증, 토큰 탈취 감지 테스트
- TokenControllerTest: API 엔드포인트 테스트 업데이트
- UserControllerTest: HttpServletRequest 목 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bbbang105 bbbang105 requested a review from anxi01 January 20, 2026 07:02
@bbbang105 bbbang105 added ⚒️ chore 빌드 부분 혹은 패키지 매니저 수정 사항 ✅ test 테스트 코드 🔄 refactor 코드 리팩토링 🔧 ci CI/CD 파이프라인 변경 🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 labels Jan 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Walkthrough

Redis 기반 분산 락·리프레시 토큰 저장소를 제거하고 JPA/MySQL 기반의 RefreshToken 엔티티로 전환했습니다. JTI 기반 토큰 로테이션(3초 그레이스)과 재발급 흐름, 클라이언트 메타데이터(IP·User‑Agent) 수집, 스케줄러·리포지토리·서비스·컨트롤러 시그니처 및 테스트·CI 설정이 함께 변경되었습니다.

Changes

Cohort / File(s) 변경 요약
CI / 빌드 / 테스트 설정
*.github/workflows/commit-labeler.yaml, *.github/workflows/prod-cicd.yaml, *.github/workflows/auto-assign.yaml, *.github/auto-assign-config.yaml, *.github/CODEOWNERS, \build.gradle`, `.gitignore``
커밋 라벨 매칭 완화·신규 라벨 추가, prod ECR 푸시 조건(merged) 추가, auto-assign 워크플로우/코드오너 수정, Testcontainers 의존성 추가, OpenAPI JSON 무시 항목 추가
도메인 모델
\src/main/java/side/onetime/domain/RefreshToken.java`, `src/main/java/side/onetime/domain/enums/TokenStatus.java``
JPA RefreshToken 엔티티 및 TokenStatus enum 추가(가족Id, jti, browserId, userIp, userAgent, 상태·발급·만료·재발급카운트 등)
영속성 계층 (리포지토리)
\src/main/java/side/onetime/repository/RefreshTokenRepository.java`, `src/main/java/side/onetime/repository/custom/*``
Redis 구현 제거, JpaRepository 기반 전환, findByJti, markAsRotatedIfActive 등 원자적 업데이트 및 QueryDSL 기반 대량 업데이트/삭제 메서드 추가
서비스 계층 (토큰 로직)
\src/main/java/side/onetime/service/TokenService.java`, `src/main/java/side/onetime/service/UserService.java`, `src/main/java/side/onetime/service/TestAuthService.java`, `src/main/java/side/onetime/service/RefreshTokenCleanupScheduler.java```
트랜잭션 기반 토큰 로테이션·재발급 도입(3초 그레이스), 상태별 분기(ROTATED 재사용 감지·가족 전체 폐기 등), 만료 전환·하드삭제 스케줄러 추가
컨트롤러 · 핸들러 · 유틸
\src/main/java/side/onetime/controller/TokenController.java`, `src/main/java/side/onetime/controller/UserController.java`, `src/main/java/side/onetime/auth/handler/OAuthLoginSuccessHandler.java`, `src/main/java/side/onetime/util/ClientInfoExtractor.java`, `src/main/java/side/onetime/util/JwtUtil.java```
컨트롤러/핸들러에 HttpServletRequest 인자 추가 및 ClientInfoExtractor 도입(IP·UA 추출), JwtUtil에 jti 포함 refresh 토큰 생성·만료 계산 API 추가
예외·설정 변경
\src/main/java/side/onetime/exception/status/TokenErrorStatus.java`, `src/main/resources/application.yaml`, `src/main/resources/application-local.yaml```
토큰 관련 오류 코드 추가(_INVALID_REFRESH_TOKEN 등), refresh-token.cleanup 스케줄 설정 추가, Redisson/분산락 관련 설정 및 파일 삭제 반영
테스트 변경
\src/test/java/.../configuration/DatabaseTestConfig.java`, `src/test/resources/application.yaml`, `src/test/java/.../token/RefreshTokenRepositoryTest.java`, `src/test/java/.../token/TokenServiceTest.java`, `src/test/java/.../token/TokenControllerTest.java`, `src/test/java/.../user/UserControllerTest.java`, `src/test/java/.../configuration/ControllerTestConfig.java```
Testcontainers 기반 MySQL 테스트 설정 추가, 리포지토리·서비스·컨트롤러 테스트 업데이트(새 시그니처·ClientInfoExtractor 목 반영)
보안·분산락 관련 제거
\src/main/java/side/onetime/global/filter/JwtFilter.java`, `src/main/java/side/onetime/global/lock/... ``
JWT 필터 예외 경로에 재발급·로그아웃 추가, DistributedLock 애노테이션·AOP·SpEL 유틸·Redisson 설정 파일 제거(분산 락 관련 코드 삭제)

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as TokenController
    participant Service as TokenService
    participant Extractor as ClientInfoExtractor
    participant Jwt as JwtUtil
    participant Repo as RefreshTokenRepo

    Client->>Controller: POST /api/v1/tokens/action-reissue (body + HttpServletRequest)
    Controller->>Extractor: extractClientIp(httpRequest)
    Extractor-->>Controller: userIp
    Controller->>Extractor: extractUserAgent(httpRequest)
    Extractor-->>Controller: userAgent
    Controller->>Service: reissueToken(request, userIp, userAgent)
    Service->>Jwt: parse provided refresh token -> jti
    Jwt-->>Service: jti
    Service->>Repo: findByJti(jti)
    Repo-->>Service: refreshTokenEntity

    alt status == ACTIVE
        Service->>Repo: markAsRotatedIfActive(tokenId, now, userIp)
        Repo-->>Service: updatedCount
        Service->>Jwt: generateAccessToken(...)
        Jwt-->>Service: newAccessToken
        Service->>Jwt: generateRefreshToken(..., newJti)
        Jwt-->>Service: newRefreshToken
        Service->>Repo: save(rotated/new RefreshToken)
        Repo-->>Service: savedEntity
        Service-->>Controller: return new tokens
    else status == ROTATED
        alt within grace period
            Service-->>Controller: throw DUPLICATED_REQUEST
        else
            Service->>Repo: revokeAllByFamilyId(familyId)
            Repo-->>Service: revokedCount
            Service-->>Controller: throw TOKEN_REUSE_DETECTED
        end
    else status in (REVOKED, EXPIRED)
        Service-->>Controller: throw INVALID_REFRESH_TOKEN
    end

    Controller-->>Client: HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • anxi01

Poem

🐰 폴짝폴짝 JTI로 달려가요,
MySQL 밭에 IP와 UA를 살포시 심고,
로테이션은 짧은 은혜, 그레이스는 숨결,
옛 토큰은 상태를 바꾸며 쉬고,
당근 하나로 배포를 축하해요 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경 사항(Redis에서 MySQL로 RefreshToken 마이그레이션)을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 모든 필수 섹션을 포함하고 있으며, 상세한 작업 내용, 관련 이슈, 기타 사항이 구체적으로 작성되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bbbang105 bbbang105 added the 😵‍💫 sangho 상호 PR label Jan 20, 2026
bbbang105 and others added 2 commits January 20, 2026 16:10
- PR 생성 시 작성자를 assignee로 자동 할당
- 팀원을 reviewer로 자동 할당 (작성자 제외)
- 설정 파일명 변경: auto_assign.yaml → auto-assign-config.yaml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CI에서 openapi3 태스크 실행 시 디렉토리 없음 오류 방지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bbbang105 bbbang105 self-assigned this Jan 20, 2026
@bbbang105 bbbang105 added the 📄 docs 문서 추가 및 수정 label Jan 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/side/onetime/service/TestAuthService.java (1)

41-68: revoke+save가 원자적이지 않습니다 — @Transactional 필요.
토큰 revoke 후 저장 실패 시 ACTIVE 토큰이 사라지는 불일치가 발생할 수 있습니다. 서비스 메서드에 트랜잭션을 걸어 원자성을 보장하세요.

🧩 제안 수정안
+import org.springframework.transaction.annotation.Transactional;
...
-    public OnboardUserResponse login(TestLoginRequest request) {
+    `@Transactional`
+    public OnboardUserResponse login(TestLoginRequest request) {
As per coding guidelines, ...
🤖 Fix all issues with AI agents
In @.github/workflows/commit-labeler.yaml:
- Around line 24-36: The current label logic uses substring checks like
msg.includes("ci") which false-positives on words such as "traffic"; change to
stricter commit-header regex matching on the msg variable (e.g. match the
conventional commit type at the start of the header) and replace the
msg.includes checks (the lines with msg.includes("feat"), msg.includes("fix"),
msg.includes("ci"), etc.) with regex tests that assert the type prefix (e.g.
/^feat(\(|:|\s)/i) so only actual commit types trigger labels via the labels.add
calls.

In `@src/main/java/side/onetime/domain/RefreshToken.java`:
- Around line 62-143: The entity currently stores the raw Refresh Token in the
tokenValue field and in create(...) and rotate(...), which is unsafe; change to
store and persist only a secure hash (e.g., SHA-256 or HMAC-SHA256) instead of
the JWT string: update the field name (tokenValue → tokenHash or keep tokenValue
but document it's a hash), change create(...) and rotate(...) to accept the raw
token for hashing (or accept the already-hashed value) and compute/assign the
hash before persisting, adjust the column definition to suit the hash length,
and ensure all token comparisons elsewhere use the same hash function to compare
incoming raw tokens against the stored hash.

In
`@src/main/java/side/onetime/repository/custom/RefreshTokenRepositoryImpl.java`:
- Around line 18-58: Bulk QueryDSL updates/delete bypass JPA auditing so you
must explicitly set refreshToken.updatedDate when changing statuses: in
revokeByUserIdAndBrowserId, revokeAllByUserId, revokeAllByFamilyId add a
.set(refreshToken.updatedDate, LocalDateTime.now()) in the update query; in
updateExpiredTokens use the provided now parameter and add
.set(refreshToken.updatedDate, now) alongside setting status; ensure the update
queries continue to call .execute() and import LocalDateTime if needed so
updatedDate reflects the status change.

In `@src/main/java/side/onetime/service/TokenService.java`:
- Around line 54-66: The reissue flow must explicitly validate expiration before
extracting claims and be protected by a distributed lock: call
jwtUtil.validateToken(refreshToken) immediately after obtaining
reissueTokenRequest.refreshToken() and before
jwtUtil.getClaimFromToken(refreshToken, "jti", String.class) so expired tokens
raise TokenErrorStatus._TOKEN_EXPIRED instead of a generic claim extraction
error; also annotate the TokenService reissue method with `@DistributedLock` (or
the appropriate lock annotation used in the project) to prevent race conditions
when accessing refreshTokenRepository.findByJti(jti) and updating/validating
RefreshToken records.

In `@src/main/java/side/onetime/util/JwtUtil.java`:
- Around line 148-156: The calculateRefreshTokenExpiryAt method currently
divides REFRESH_TOKEN_EXPIRATION_TIME by 1000 losing sub-second precision;
update JwtUtil.calculateRefreshTokenExpiryAt to add the expiration using
millisecond (or nanosecond) precision instead (e.g., use
Duration.ofMillis(REFRESH_TOKEN_EXPIRATION_TIME) or
issuedAt.plus(REFRESH_TOKEN_EXPIRATION_TIME, ChronoUnit.MILLIS)) so issuedAt is
incremented exactly by REFRESH_TOKEN_EXPIRATION_TIME without truncation.
🧹 Nitpick comments (9)
build.gradle (1)

98-102: Testcontainers 버전 정합성 확보 권장.
junit-jupiter(1.19.0)와 mysql(1.20.1) 버전이 달라 충돌 가능성이 있습니다. BOM으로 정렬하는 편이 안전합니다.

🔧 제안 수정안
+    testImplementation platform('org.testcontainers:testcontainers-bom:1.20.1')
     testImplementation 'org.springframework.boot:spring-boot-testcontainers'
-    testImplementation 'org.testcontainers:testcontainers'
-    testImplementation 'org.testcontainers:junit-jupiter:1.19.0'
-    testImplementation 'org.testcontainers:mysql:1.20.1'
+    testImplementation 'org.testcontainers:testcontainers'
+    testImplementation 'org.testcontainers:junit-jupiter'
+    testImplementation 'org.testcontainers:mysql'
src/main/java/side/onetime/util/ClientInfoExtractor.java (1)

15-38: X-Forwarded-For 신뢰 경계 확인 필요.
헤더 직접 파싱은 스푸핑 위험이 있어, 신뢰 가능한 프록시 뒤에서만 사용되는지 확인하고 가능하면 ForwardedHeaderFilter/request.getRemoteAddr() 기반으로 일원화하는 편이 안전합니다.

src/main/java/side/onetime/service/TestAuthService.java (1)

58-60: 시간 기준 일관성 개선 제안.
now를 한 번만 찍고 동일 값을 만료 계산에 재사용하면 미세한 시간 차이를 제거할 수 있습니다.

🧩 제안 수정안
-        LocalDateTime now = LocalDateTime.now();
-        LocalDateTime expiryAt = jwtUtil.calculateRefreshTokenExpiryAt(LocalDateTime.now());
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime expiryAt = jwtUtil.calculateRefreshTokenExpiryAt(now);
src/test/java/side/onetime/configuration/DatabaseTestConfig.java (1)

28-43: @ServiceConnectionDynamicPropertySource가 중복됩니다.

Spring Boot 3.1+ (현 프로젝트는 3.3.2)에서 @ServiceConnection은 자동으로 Testcontainers로부터 DataSource를 구성합니다. DynamicPropertySource와 함께 사용하면 중복 설정이 됩니다. @ServiceConnection만 사용하면 됩니다.

♻️ `@ServiceConnection만` 사용하는 방식 (권장)
    `@ServiceConnection`
    static final MySQLContainer<?> MYSQL_CONTAINER;

    static {
        MYSQL_CONTAINER = new MySQLContainer<>(MYSQL_IMAGE)
                .withCommand("--default-time-zone=+09:00")
                .withLogConsumer(new Slf4jLogConsumer(log));
        MYSQL_CONTAINER.start();
    }
-
-    `@DynamicPropertySource`
-    static void configureProperties(DynamicPropertyRegistry registry) {
-        registry.add("spring.datasource.url", MYSQL_CONTAINER::getJdbcUrl);
-        registry.add("spring.datasource.username", MYSQL_CONTAINER::getUsername);
-        registry.add("spring.datasource.password", MYSQL_CONTAINER::getPassword);
-    }
src/main/java/side/onetime/domain/RefreshToken.java (1)

30-38: 도메인 Soft Delete 패턴 미적용

도메인 가이드라인은 @SQLDelete/@SQLRestriction + Status(ACTIVE/DELETED) 적용을 요구합니다. RefreshToken은 하드 삭제 로직이 있어 예외라면 정책 문서화가 필요하고, 가능하면 Soft Delete + 스케줄러 purge로 정렬하는 방향을 검토해 주세요. 코딩 가이드라인에 근거함(As per coding guidelines).

src/test/java/side/onetime/token/TokenServiceTest.java (2)

32-34: 테스트 유형(단위/통합)과 @SpringBootTest 사용 여부를 확인해주세요.

현재 MockitoExtension 기반 단위 테스트인데, 가이드라인이 Spring Boot Test 사용을 요구하는지 정책 확인이 필요합니다. As per coding guidelines, ...


68-92: 리플렉션 기반 필드 조작은 테스트 취약점입니다.

private 필드 조작이 깨지기 쉬워 유지보수에 불리합니다. 테스트 전용 팩토리/빌더 또는 패키지 스코프 설정자 추가를 고려해 주세요.

src/main/java/side/onetime/service/TokenService.java (1)

52-53: @distributedlock 제거가 정책적으로 의도된 것인지 확인이 필요합니다.

현재 원자적 업데이트로 레이스를 방지하고 있지만, 서비스 메서드에 @DistributedLock 사용을 요구하는 가이드라인과의 정합성을 확인해 주세요. As per coding guidelines, ...

src/main/java/side/onetime/repository/RefreshTokenRepository.java (1)

34-46: @ModifyingclearAutomatically = true 추가는 선택사항

현재 코드는 markAsRotatedIfActive() 호출 후 동일 트랜잭션 내에서 업데이트된 엔티티를 다시 조회하지 않으므로, clearAutomatically = true 추가는 필수는 아닙니다. 다만 향후 이 메서드 호출 이후 같은 토큰을 재조회하는 로직이 추가될 가능성을 대비한 방어적 코딩으로 추가하는 것도 좋습니다.

또한 RefreshToken.status 필드에 @Enumerated(EnumType.STRING)이 올바르게 설정되어 있으므로, JPQL에서 문자열 리터럴('ACTIVE', 'ROTATED')로 비교하는 것이 정상 작동합니다. ✅

- RefreshTokenRepositoryImpl의 update/delete 메서드에 @transactional 추가
- TestAuthService.login()에 @transactional 추가
- Security Filter에서 호출 시 트랜잭션 없이 실행되는 문제 해결

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@build.gradle`:
- Around line 95-99: The Testcontainers dependencies specify mixed, hard-coded
versions (junit-jupiter:1.19.0 and mysql:1.20.1) which can cause
incompatibility; remove the explicit version qualifiers from the Testcontainers
module declarations in build.gradle (the testImplementation entries for
'org.testcontainers:testcontainers', 'org.testcontainers:junit-jupiter', and
'org.testcontainers:mysql') so Spring Boot's dependency management
(testcontainers.version) controls a single consistent version, and keep the
Spring Boot testcontainers artifact
('org.springframework.boot:spring-boot-testcontainers') as-is without adding
explicit Testcontainers versions.

In `@src/main/resources/application-local.yaml`:
- Around line 4-6: The YAML enables SQL initialization but no scripts exist:
create src/main/resources/schema.sql containing the required DDL (table
definitions) and optionally src/main/resources/data.sql for initial rows to
satisfy sql.init.mode: always; alternatively, if you prefer Hibernate to manage
schema, either remove generate-ddl and sql.init.mode or change
spring.jpa.hibernate.ddl-auto from validate to create/update (e.g., ddl-auto:
update) so startup succeeds; ensure the keys referenced (sql.init.mode,
generate-ddl, spring.jpa.hibernate.ddl-auto / ddl-auto) are consistent with the
chosen approach.

bbbang105 and others added 2 commits January 21, 2026 13:32
- generate-ddl 제거 (ddl-auto와 중복)
- ddl-auto: validate → update
- sql.init.mode: always → never

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
# Conflicts:
#	src/main/java/side/onetime/service/TestAuthService.java
#	src/main/java/side/onetime/util/JwtUtil.java
Copy link
Member

@anxi01 anxi01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! RTR을 잘 몰랐는데 많이 배웁니다~

HttpServletRequest httpRequest) {

ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest);
ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest, httpRequest);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service 레이어에서 httpRequest를 인자로 받고 있어 HTTP 요청과 비즈니스 로직이 강결합이 될 수 있을 것 같아요!
Controller에서 HttpRequest의 userAgent, clientIp를 추출해서 Service 레이어에 넘겨주면 해소할 수 있을 것 같습니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분을 놓쳤네요 감사합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpServletRequest httpRequest) {

OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest);
OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest, httpRequest);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 위 리뷰와 마찬가지로
Controller에서 HttpRequest의 userAgent, clientIp를 추출해서 Service 레이어에 넘겨주면 해소할 수 있을 것 같습니다~

- Controller에서 ClientInfoExtractor를 사용해 userIp, userAgent 추출
- Service는 추출된 값을 파라미터로 받도록 변경
- HTTP 요청과 비즈니스 로직의 결합도 감소

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/java/side/onetime/service/TokenService.java`:
- Around line 105-109: The access token is being generated with a hardcoded
"USER" role which downgrades admins; change the call to
jwtUtil.generateAccessToken(...) to use the original user type stored on
oldToken (e.g., oldToken.getUserType() or the actual getter used on the Token
object) instead of the literal "USER", so the newAccessToken preserves the
user's original role; ensure you reference oldToken and
jwtUtil.generateAccessToken to locate and update the code.

In `@src/test/java/side/onetime/token/TokenServiceTest.java`:
- Around line 30-32: Replace the pure Mockito JUnit setup in TokenServiceTest by
switching the class-level annotation from `@ExtendWith`(MockitoExtension.class) to
a Spring Boot Test annotation (e.g., `@SpringBootTest` or another more specific
slice if applicable) so the test follows the project's Spring Boot Test
convention; update any Mockito-only wiring to use Spring-managed beans or
`@MockBean` as needed for TokenServiceTest, and refactor the createTestToken
helper (the method named createTestToken that currently manipulates private
fields via reflection) into a maintainable test helper or Builder-style factory
that constructs tokens through constructors or setters instead of direct
reflective field access.
🧹 Nitpick comments (1)
src/test/java/side/onetime/token/TokenServiceTest.java (1)

58-82: 리플렉션으로 private 필드 조작은 유지보수에 취약합니다.
필드명 변경에 쉽게 깨질 수 있으니, 테스트 전용 팩토리/빌더(패키지-프라이빗)로 status/lastUsedAt/id를 설정할 수 있게 여는 방식을 고려해 주세요.

- 토큰 재발급 시 기존 userType을 유지하도록 개선
- 관리자 토큰 재발급 시 권한 강등 버그 방지
- RefreshToken 엔티티, JWT, Service 레이어 수정

Co-Authored-By: Claude <noreply@anthropic.com>
@bbbang105 bbbang105 merged commit 08efa9c into develop Jan 25, 2026
4 checks passed
@bbbang105 bbbang105 deleted the feature/#315/refresh-token branch January 25, 2026 05:01
bbbang105 added a commit that referenced this pull request Jan 25, 2026
* [refactor]: 배너 API를 Admin에서 Banner 도메인으로 이동한다 (#310)

* [feat] : JwtFilter의 인증 로직을 Security FilterChain 으로 통합한다 (#312)

* #311 [feat] : AdminUser를 UserDetails 객체로 래핑한다

* #311 [feat] : SecurityContext에서 Admin ID를 조회하도록 변경한다

* #311 [feat] : 테스트 환경에서 SecurityContext에 Admin, User를 분리하여 주입하도록 변경한다

* #311 [feat] : CustomUserDetails에 User Authority를 설정한다

* #311 [feat] : Security Filterchain에서 401, 403 예외를 핸들링한다

* #311 [feat] : Security Filterchain에서 요청의 인증, 인가를 관리한다

* #311 [feat] : JwtFilter에서 토큰으로 Admin, User를 비교하여 SecurityContext에 주입한다

* #311 [refactor] : UserDetails의 default method를 제거한다

* #311 [feat] : Authentication NPE 방지 및 Admin 401 ErrorStatus를 추가한다

* #311 [refactor] : SecurityContext에서 객체를 가져오지 못하는 경우의 에러타입을 변경한다

* [feat] : 테스트 로그인을 구현한다 (#314)

* [feat] : 테스트 로그인용 설정값을 추가한다

* [docs] : 클로드 문서를 정리한다

* [feat] : 테스트 로그인 API를 구현한다

* feat: Http 코드를 통일한다

* fix: 테스트코드를 수정한다

---------

Co-authored-by: sh.h <sh.h@logosai.co.kr>

* feat: E2E 테스트를 위한 만료된 토큰을 반환하는 API를 구현한다 (#318)

* chore: 커밋 컨벤션을 업데이트한다

- 대괄호 없는 형식으로 변경 (feat: ~)
- 이슈번호 제외
- 커밋 제외 파일 명시 (docs, openapi)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: 만료된 액세스 토큰 발급 테스트 API를 추가한다

- POST /api/v1/test/auth/expired-token 엔드포인트 추가
- TestTokenResponse DTO 신규 생성 및 기존 테스트 로그인 API에도 적용
- JwtUtil에 generateExpiredAccessToken 메서드 추가
- E2E 테스트에서 401 처리, 토큰 재발급 플로우 테스트에 사용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: 만료 토큰 API 응답 상태를 200 OK로 변경한다

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Swagger 예외 케이스 문서화 추가

- 테스트 실패 케이스에 MockMvcRestDocumentationWrapper 추가
- CLAUDE.md에 예외 케이스 문서화 컨벤션 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: CORS 오리진을 추가한다

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: 리프레쉬 토큰을 RDBMS에서 관리한다 (#316)

* feat: RefreshToken 저장소를 Redis에서 MySQL로 마이그레이션

- RefreshToken 엔티티에 token rotation 관련 필드 추가
- TokenStatus enum 추가 (ACTIVE, REVOKED, EXPIRED, ROTATED)
- 원자적 업데이트를 위한 markAsRotatedIfActive 메서드 추가
- QueryDSL 기반 커스텀 Repository 구현 (revoke, hard delete)
- 새로운 에러 코드 추가 (_TOKEN_REUSE_DETECTED, _DUPLICATED_REQUEST, _ALREADY_USED_REFRESH_TOKEN)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: 토큰 재발급 API에 원자적 업데이트 및 토큰 검증 추가

- Race condition 방지를 위한 원자적 토큰 상태 업데이트
- 토큰 값 검증 로직 추가 (DB 토큰과 요청 토큰 비교)
- Grace Period 내 중복 요청 처리
- 토큰 재사용 공격 탐지 시 family 전체 revoke

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: ClientInfoExtractor 유틸 추가 및 토큰에 IP/UserAgent 저장

- ClientInfoExtractor 유틸 클래스 추가 (IP, UserAgent 추출)
- OAuth 로그인 및 온보딩 시 클라이언트 정보 저장
- 테스트 로그인에도 클라이언트 정보 저장 적용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: 회원 탈퇴 시 RefreshToken revoke 처리

- 회원 탈퇴(withdraw) 시 해당 유저의 모든 ACTIVE 토큰을 REVOKED로 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: JwtUtil에 expiryAt 계산 메서드 추출

- calculateRefreshTokenExpiryAt 메서드 추가
- 토큰 만료 시간 계산 로직 중복 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [feat] : RefreshToken Cleanup Scheduler 추가

- 만료된 토큰 상태 업데이트 (ACTIVE → EXPIRED) 스케줄러 추가
- 오래된 비활성 토큰(REVOKED/EXPIRED/ROTATED) hard delete 스케줄러 추가
- 스케줄러 cron 및 retention-days 설정을 YAML로 외부화
- 기본값: 매일 03:00 만료 처리, 03:30 hard delete, 30일 보관

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [test] : DB 통합 테스트를 위한 Testcontainers 설정 추가

- Testcontainers MySQL 의존성 추가 (H2 제거)
- Singleton Container Pattern으로 DatabaseTestConfig 구현
- 테스트 환경에서 실제 MySQL과 동일한 환경으로 테스트 가능
- @Serviceconnection과 @DynamicPropertySource로 DB 연결 자동화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [test] : RefreshToken 관련 테스트 코드 추가

- RefreshTokenRepositoryTest: Repository 레이어 통합 테스트
- TokenServiceTest: 토큰 재발급, 검증, 토큰 탈취 감지 테스트
- TokenControllerTest: API 엔드포인트 테스트 업데이트
- UserControllerTest: HttpServletRequest 목 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [ci] : commit-labeler에 ci 라벨 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [ci] : prod-cicd PR 머지 시에만 실행되도록 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [chore] : open-api JSON 파일 gitignore 처리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [ci] : PR Auto Assign 워크플로우 추가

- PR 생성 시 작성자를 assignee로 자동 할당
- 팀원을 reviewer로 자동 할당 (작성자 제외)
- 설정 파일명 변경: auto_assign.yaml → auto-assign-config.yaml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [chore] : CODEOWNERS에 팀원 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [chore] : static/docs 디렉토리 gitkeep 추가

- CI에서 openapi3 태스크 실행 시 디렉토리 없음 오류 방지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: RefreshToken 관련 @transactional 누락 수정

- RefreshTokenRepositoryImpl의 update/delete 메서드에 @transactional 추가
- TestAuthService.login()에 @transactional 추가
- Security Filter에서 호출 시 트랜잭션 없이 실행되는 문제 해결

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: QueryDSL bulk update 시 updatedDate 수동 갱신 추가

- JPA Auditing이 bulk update를 우회하므로 updatedDate 수동 설정
- hardDeleteOldInactiveTokens가 updatedDate 기준으로 삭제하므로 필수

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: 토큰 재발급 API에서 JwtFilter 제외

- /api/v1/tokens/action-reissue는 만료된 액세스 토큰으로 요청
- JwtFilter.shouldNotFilter()에 해당 경로 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: 로그아웃 API에서 JwtFilter 제외

- /api/v1/users/logout도 만료된 액세스 토큰으로 요청 가능
- shouldNotFilter에 해당 경로 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: LocalDateTime.now() 중복 호출 제거

- TestAuthService에서 now 변수 재사용하도록 수정
- 미세한 시간 차이로 인한 테스트/로깅 불일치 방지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: TokenService에 validateToken() 추가

- getClaimFromToken() 호출 전 토큰 검증 추가
- 만료된 토큰에 대해 명확한 에러 메시지 반환

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: 미사용 DistributedLock 관련 코드 제거

- DistributedLock 어노테이션 제거
- DistributedLockAop 제거
- CustomSpringELParser 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: redisson 의존성 제거

* feat: 로컬 yaml db 설정 변경

* fix: local 프로파일 JPA 설정 개선

- generate-ddl 제거 (ddl-auto와 중복)
- ddl-auto: validate → update
- sql.init.mode: always → never

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: 최신 버전을 사용한다

* fix: gitkeep을 제거하지 않도록 변경한다

* fix: conflict를 해결한다

* fix: 슬래시를 제거한다

* refactor: Service 레이어에서 HttpServletRequest 의존성을 제거한다

- Controller에서 ClientInfoExtractor를 사용해 userIp, userAgent 추출
- Service는 추출된 값을 파라미터로 받도록 변경
- HTTP 요청과 비즈니스 로직의 결합도 감소

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: RefreshToken에 userType 필드를 추가한다

- 토큰 재발급 시 기존 userType을 유지하도록 개선
- 관리자 토큰 재발급 시 권한 강등 버그 방지
- RefreshToken 엔티티, JWT, Service 레이어 수정

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: SeongMin Han <123073840+anxi01@users.noreply.github.com>
Co-authored-by: sh.h <sh.h@logosai.co.kr>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚒️ chore 빌드 부분 혹은 패키지 매니저 수정 사항 🔧 ci CI/CD 파이프라인 변경 📄 docs 문서 추가 및 수정 🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 🚨 fix 버그 수정 / 에러 해결 🔄 refactor 코드 리팩토링 😵‍💫 sangho 상호 PR ✅ test 테스트 코드

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 리프레쉬 토큰을 RDBMS에서 관리한다

3 participants